package br.ufpe.cin.imlm2.jbeat.soot.dump;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Calendar;
import java.util.Collection;
import java.util.Map;

import soot.SceneTransformer;
import soot.SootClass;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightScene;
import br.ufpe.cin.imlm2.jbeat.soot.common.SceneWrapper;
import br.ufpe.cin.imlm2.util.options.IOptions;
import br.ufpe.cin.imlm2.util.options.NoSuchOptionException;
import br.ufpe.cin.imlm2.util.options.Parameterizable;
import br.ufpe.cin.imlm2.util.options.ParsingException;

public class ClassDumper extends SceneTransformer implements Parameterizable {

	private static final int DUMP_THREAD_STACK_FRAME_SIZE = 6291456;
	protected static final String EXTENSION_JB = ".jb";
	private DumpOptions options;
	private File outputPath;
	private Exception writeException;

	public ClassDumper() {
		this.options = new DumpOptions();
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void internalTransform( String phaseName, Map options ) {
		Long startTime = System.currentTimeMillis();
		System.err.println( "Class dumping begining at " + Calendar.getInstance().getTime() );
		try {
			this.options.parse( options );
		} catch ( NoSuchOptionException e ) {
			System.err.println( "Invalid option '" + e.getInvalidOtion()
					+ "' being ignored. Remaining options being ignored." );
		} catch ( ParsingException e ) {
			throw new RuntimeException( "Unable to parse phase options.", e );
		}

		outputPath = this.getOutputPath();
		try {
			final ObjectOutputStream outStream = this.getOutputStream( outputPath );

			Collection<SootClass> classes = SceneWrapper.getClasses();
			ClassResolver resolver = new ClassResolver();
			LightWeightScene scene = resolver.resolveSootClasses( classes );
			System.err.println( "Writing LightWeightScene with " + scene.getClasses().size()
					+ " classes and " + scene.getTypes().size() + " types." );
			System.err.println( "Soot Scene containts " + SceneWrapper.getClasses().size()
					+ " classes of which " + SceneWrapper.getClasses().size()
					+ " are Java classes " + "(concrete and abstract but not interfaces)." );
			Thread dumpThread = new Thread( null, new DumpWriter( scene, outStream ),
					"Dump Thread", DUMP_THREAD_STACK_FRAME_SIZE );
			dumpThread.start();
			dumpThread.join();
			if ( this.writeException != null )
				throw new DumpException( "Failed to write to output file", this.writeException );

		} catch ( FileNotFoundException e ) {
			throw new DumpException( "Failed to write to output file", e );
		} catch ( IOException e ) {
			throw new DumpException( "Failed to write to output file", e );
		} catch ( InterruptedException e ) {
			throw new DumpException( "Failed to write to output file", e );
		}
		System.err.println( "Class dumping finished in " + (System.currentTimeMillis() - startTime)
				+ "ms at " + Calendar.getInstance().getTime() );

	}

	private class DumpWriter implements Runnable {

		private LightWeightScene scene;
		private ObjectOutputStream out;

		public DumpWriter( LightWeightScene scene, ObjectOutputStream out ) {
			super();
			this.scene = scene;
			this.out = out;
		}

		@Override
		public void run() {
			try {
				this.out.writeObject( this.scene );
				this.out.close();
			} catch ( IOException e ) {
				ClassDumper.this.writeException = e;
			}

		}

	}

	private ObjectOutputStream getOutputStream( File outPath ) throws FileNotFoundException,
			IOException {
		ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream( outPath, false ) );
		return oos;
	}

	private File getOutputPath() {
		String path = this.options.getDumpPath();
		if ( path == null || path.isEmpty() )
			try {
				path = (String) this.options.getOption( DumpOptions.DUMP_PATH ).getDefaultValue();
			} catch ( NoSuchOptionException e1 ) {
				throw new DumpException( "Unable to created dump output file.", e1 );
			}
		File outpath = new File( path );
		try {
			File parent = outpath.getParentFile();
			if ( parent != null && !parent.exists() ) {
				parent.mkdirs();
			}
			if ( !outpath.exists() ) {
				outpath.createNewFile();
			}
		} catch ( IOException e ) {
			throw new DumpException( "Unable to created dump output file.", e );
		}

		return outpath;
	}

	@Override
	public IOptions getOptions() {
		return this.options;
	}
}
